package com.artnology.independence.reengineering;

import java.lang.reflect.Modifier;
import java.util.Set;

import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;

import com.artnology.independence.Activator;
import com.artnology.independence.model.DVType;
import com.artnology.independence.model.relationships.DVAbstractTypeRelationship;
import com.artnology.independence.model.relationships.DVExtensionRelationship;
import com.artnology.independence.model.relationships.DVImplementsRelationship;
import com.artnology.independence.model.relationships.DVOneToManyRelationship;
import com.artnology.independence.model.relationships.DVOneToOneRelationship;
import com.artnology.independence.model.relationships.DVReference;
import com.artnology.independence.model.relationships.DVUsesRelationship;

public class ParsingThread extends Thread {

	static Class<?> collectionInterface = null;
	static Class<?> mapInterface = null;

	static ILog logger = Activator.getDefault().getLog();
	Set<ITypeRoot> allTypes;
	IJavaElement[] oneJavaElement = null;
	IProgressMonitor pm;
	int id;

	static {
		try {
			collectionInterface = Class.forName("java.util.Collection");
			mapInterface = Class.forName("java.util.Map");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	public ParsingThread(int id, IProgressMonitor pm, Set<ITypeRoot> allTypes) {
		this.allTypes = allTypes;
		this.pm = pm;
		this.id = id;
		oneJavaElement = new IJavaElement[] { null };
	}

	// @Override
	public void run() {
		for (ITypeRoot myType : allTypes) {
			
			DVType dvType = DVType.getDVTypeForJDTType(myType);
			if (myType instanceof ICompilationUnit) {
				ICompilationUnit compUnit = (ICompilationUnit) myType;
				try {
					oneJavaElement[0] = compUnit.getTypes()[0];
				} catch (JavaModelException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else {
				// we got class file
				IClassFile classFile = (IClassFile) myType;
				oneJavaElement[0] = classFile.getType();
			}
			ASTParser bindingParser = ASTParser.newParser(AST.JLS4);
			bindingParser.setProject(oneJavaElement[0].getJavaProject());
			ITypeBinding typeBinding = (ITypeBinding) bindingParser
					.createBindings(oneJavaElement, null)[0];
			processTypeBinding(dvType, typeBinding);
			// process fields
			IVariableBinding[] allFields = typeBinding.getDeclaredFields();
			for (IVariableBinding iVariableBinding : allFields) {
				
				processTypeBinding(dvType, iVariableBinding.getType(),
						iVariableBinding.getName());
			}

			// process method parameters and return types
			IMethodBinding[] allMethods = typeBinding.getDeclaredMethods();
			for (IMethodBinding iMethodBinding : allMethods) {

				if (isFieldGetterOrSetterMethodBinding(dvType, iMethodBinding)) {
					continue;
				}

				ITypeBinding[] allParmTypeBindings = iMethodBinding
						.getParameterTypes();
				for (ITypeBinding parmTypeBinding : allParmTypeBindings) {
					
					processTypeBinding(dvType, parmTypeBinding, null);
				}

			
				processTypeBinding(dvType, iMethodBinding.getReturnType(), null);

				ITypeBinding[] allExcTypeBindings = iMethodBinding
						.getExceptionTypes();
				for (ITypeBinding excTypeBinding : allExcTypeBindings) {
					processTypeBinding(dvType, excTypeBinding, null);
				}
			}
			if (pm != null) {
				pm.subTask("Parsing Relationships for type " + dvType.getName()
						+ "...");
				pm.worked(4);
				if (pm.isCanceled()) {
					throw new OperationCanceledException();
				}
			}
		}
	}

	private static final boolean isFieldGetterOrSetterMethodBinding(
			DVType type, IMethodBinding methodBinding) {
		String methodName = methodBinding.getName().toLowerCase();
		for (DVReference reference : type.getReferencesTo()) {
			if (reference.getName() != null) {
				String setterName = ("set" + reference.getName()).toLowerCase();
				String getterName = ("get" + reference.getName()).toLowerCase();
				if (methodName.equals(setterName)) {
					if (methodBinding.getParameterTypes().length == 1) {
						return true;
					}
				} else if (methodName.equals(getterName)) {
					if (methodBinding.getParameterTypes().length == 0) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private static final void processTypeBinding(DVType modelType,
			ITypeBinding typeBinding) {
		if (typeBinding == null) {
			IStatus status = new Status(
					IStatus.ERROR,
					Activator.PLUGIN_ID,
					"processTypeBinding(DVType modelType, ITypeBinding typeBinding) typeBinding NULL for node: "
							+ modelType.getFullyQualifiedName());
			logger.log(status);
		}

		if (!typeBinding.isTopLevel()) {
			// we ignore nested types
			return;
		}

		if (typeBinding.isInterface()) {
			modelType.setInterfaceType(true);
		}
		if (typeBinding.getSuperclass() != null
				&& typeBinding.getSuperclass().isTopLevel()) {
			String superclassName = typeBinding.getSuperclass().getErasure()
					.getQualifiedName();
		
			setupExtensionRelationship(modelType, superclassName);
		}
		ITypeBinding[] interfaceBindings = typeBinding.getInterfaces();
		for (int i = 0; i < interfaceBindings.length; i++) {
			// only setup relationship for top level types
			if (interfaceBindings[i].isTopLevel()) {
				setupInterfaceRelationship(modelType, interfaceBindings[i]
						.getErasure().getQualifiedName());
			}
		}
	}

	private static final void processTypeBinding(DVType modelType,
			ITypeBinding typeBinding, String name) {
		if (typeBinding == null) {
			IStatus status = new Status(
					IStatus.ERROR,
					Activator.PLUGIN_ID,
					"processVariableBinding(DVType modelType, ITypeBinding typeBinding) typeBinding NULL for node: "
							+ modelType.getFullyQualifiedName());
			logger.log(status);
			return;
		}

		DVType referencedType = null;
		boolean isOneToMany = false;

		if (!typeBinding.isTopLevel() || typeBinding.isPrimitive()
				|| typeBinding.isTypeVariable()) {
			return;
		} else if (typeBinding.isArray()) {
			isOneToMany = true;
			if (typeBinding.getElementType().isPrimitive()
					|| !typeBinding.getElementType().isTopLevel()) {
				return;
			} else {
				referencedType = getOrCreateDVType(typeBinding.getElementType()
						.getErasure().getQualifiedName());
			}
		} else if (typeBinding.isParameterizedType()) {
			// check erasure whether collection or map type (1 : n
			// relationship)
			ITypeBinding erasureType = typeBinding.getErasure();

			Class<?> erasureClass = null;
			try {
				erasureClass = Class.forName(erasureType.getQualifiedName());
			} catch (ClassNotFoundException e) {
				// ignore this erasure class and return
				return;
			}
			if (collectionInterface.isAssignableFrom(erasureClass)
					|| mapInterface.isAssignableFrom(erasureClass)) {
				isOneToMany = true;
				short typeArgumentArrayIndex = -1;
				if (collectionInterface.isAssignableFrom(erasureClass)) {
					typeArgumentArrayIndex = 0;
				} else {
					// associate value type, not key type for maps
					typeArgumentArrayIndex = 1;
				}
				ITypeBinding typeArgumentBinding = typeBinding
						.getTypeArguments()[typeArgumentArrayIndex];
				if (typeArgumentBinding.isTypeVariable()
						|| typeArgumentBinding.isWildcardType()
						|| !typeArgumentBinding.isTopLevel()) {
					// we have to ignore that reference
					return;
				} else {
					referencedType = getOrCreateDVType(typeArgumentBinding
							.getErasure().getQualifiedName());
				}
			} else {
				// no one to many type, so create ref to erasure type
				isOneToMany = false;
				referencedType = getOrCreateDVType(erasureType
						.getQualifiedName());
			}
		} else {
			// default, just a normal 1 : 1 reference
			isOneToMany = false;
			referencedType = getOrCreateDVType(typeBinding.getQualifiedName());
		}

		if (ignoreField(referencedType, modelType, typeBinding)) {
			return;
		}

		createRelationship(isOneToMany, modelType, referencedType, name);

	}

	private static final boolean ignoreField(DVType referencedType,
			DVType referenceFromType, ITypeBinding typeBinding) {

		if (referencedType != null && referencedType.equals(referenceFromType)) {
			if ((typeBinding.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
				// ignore static self references in order to not pollute the
				// graph
				return true;
			}
			// ignore self references for interfaces or enumerations
			if (typeBinding.isInterface() || typeBinding.isEnum()) {
				return true;
			}
		}
		return false;
	}

	private static final void setupExtensionRelationship(DVType type,
			String superclassType) {
		if (superclassType != null && !(type.isInterfaceType())) {
			DVType extendedType = getOrCreateDVType(superclassType);
			DVExtensionRelationship extRel = new DVExtensionRelationship(
					Activator.getDefault().getModel(), extendedType, type);
			type.addReferenceTo(extRel);

		}
	}

	private static final void setupInterfaceRelationship(DVType type,
			String interfaceTypeString) {
		DVType interfaceType = getOrCreateDVType(interfaceTypeString);
		if (!type.isInterfaceType()) {
			DVImplementsRelationship implRel = new DVImplementsRelationship(
					Activator.getDefault().getModel(), interfaceType, type);
			type.addReferenceTo(implRel);
		} else if (type.isInterfaceType()) {
			DVExtensionRelationship extRel = new DVExtensionRelationship(
					Activator.getDefault().getModel(), interfaceType, type);
			type.addReferenceTo(extRel);
		}
	}

	private static final void createRelationship(boolean oneToMany,
			DVType type, DVType referencedType, String fieldName) {

		DVAbstractTypeRelationship referenceTo = null;
		if (!oneToMany && fieldName != null) {
			referenceTo = new DVOneToOneRelationship(fieldName, Activator
					.getDefault().getModel(), referencedType, type);
		} else if (oneToMany && fieldName != null) {
			referenceTo = new DVOneToManyRelationship(fieldName, Activator
					.getDefault().getModel(), referencedType, type);

		} else if (fieldName == null) {
			referenceTo = new DVUsesRelationship(Activator.getDefault()
					.getModel(), referencedType, type);

		} else {
			// nothing matched
			return;
		}
		type.addReferenceTo(referenceTo);
	}

	private static final DVType getOrCreateDVType(String fqTypeName) {
		IStatus status = new Status(IStatus.INFO, Activator.PLUGIN_ID,
				"resolveDVType(DVType baseType, String simpleTypeName) : "
						+ fqTypeName);
		logger.log(status);

		DVType resolvedType = Activator.getDefault().getModel()
				.getTypeForFQName(fqTypeName);
		if (resolvedType == null) {
			// create new phantom type if not yet in model
			DVType newPhantom = new DVType(fqTypeName.substring(fqTypeName
					.lastIndexOf('.') + 1), fqTypeName.substring(0,
					fqTypeName.lastIndexOf('.')), Activator.getDefault()
					.getModel());
			// put phantom if not exist in type map yet
			resolvedType = Activator.getDefault().getModel()
					.addPhantomType(newPhantom);
			if (resolvedType == null) {
				resolvedType = newPhantom;
			}
		}
		return resolvedType;

	}

}